package edu.thu.thss.yxy.sip;

import java.net.InetAddress;

import javax.sip.*;
import javax.sip.address.Address;
import javax.sip.header.*;
import javax.sip.message.Request;
import javax.sip.message.Response;

import edu.thu.thss.yxy.sip.event.RegistrationStateChangeEvent;
import edu.thu.thss.yxy.util.Logger;

/**
 * Contains all functionality that has anything to do with registering and
 * maintaining registrations with a SIP Registrar.
 * 
 * @author Andy Gao
 * 
 */
public class SipRegistrarProcessor {
	/**
	 * Our class logger.
	 */
	private static final Logger logger = Logger
			.getLogger(SipRegistrarProcessor.class);

	/**
	 * A reference to the sip provider that created us.
	 */
	private SipManager sipMgrCallback = null;

	/**
	 * The InetAddress of the registrar we are connecting to.
	 */
	private InetAddress registrarAddress = null;

	/**
	 * Keeps our current registration state.
	 */
	private RegistrationState currentRegistrationState = RegistrationState.UNREGISTERED;

	/**
	 * The client transaction that we used for sending the last REGISTER
	 * request.
	 */
	ClientTransaction regTrans = null;

	public SipRegistrarProcessor(SipManager sipMgrCallback) {
		this.sipMgrCallback = sipMgrCallback;
	}

	/**
	 * Sends the REGISTER request to the server specified in the constructor.
	 * 
	 * @throws OperationFailedException
	 *             with the corresponding error code if registration or
	 *             construction of the Register request fail.
	 */
	void register() throws OperationFailedException {
		// skip REGISTERING event if we are already registered
		// we are refreshing our registration
		FromHeader fromHeader = sipMgrCallback.getFromHeader();
        Address fromAddress = fromHeader.getAddress();
        
		if (getRegistrationState() != RegistrationState.REGISTERED)
			setRegistrationState(RegistrationState.REGISTERED,
					RegistrationStateChangeEvent.REASON_NOT_SPECIFIED, fromAddress.toString());
	}

	/**
	 * Sends a unregistered request to the registrar thus ending our
	 * registration.
	 * 
	 * @throws OperationFailedException
	 *             with the corresponding code if sending or constructing the
	 *             request fails.
	 */
	public void unregister() throws OperationFailedException {
		if (getRegistrationState() == RegistrationState.UNREGISTERED) {
			logger.trace("Trying to unregister when already unresgistered");
			return;
		}

		setRegistrationState(RegistrationState.UNREGISTERED,
				RegistrationStateChangeEvent.REASON_USER_REQUEST, "");
	}

	/**
	 * An ok here means that our registration has been accepted or terminated
	 * (depending on the corresponding REGISTER request). We change state notify
	 * listeners and (in the case of a new registration) schedule
	 * reregistration.
	 * 
	 * @param clientTransatcion
	 *            the ClientTransaction that we created when sending the
	 *            register request.
	 * @param response
	 *            the OK Response that we've just received.
	 */
	public void processOK(ClientTransaction clientTransatcion, Response response) {
		// first extract the expires value that we requested
		int requestedExpiration = 0;
		Request register = clientTransatcion.getRequest();

		// first check for an expires param in the contact header
		ContactHeader contactHeader = (ContactHeader) register
				.getHeader(ContactHeader.NAME);
		if (contactHeader != null)
			requestedExpiration = contactHeader.getExpires();
		else
			requestedExpiration = 0;

		// Try the expires header only if there was no expiration interval in
		// the contact address. (Bug report and fix thereof Frederic Fournier)
		if (requestedExpiration <= 0) {
			ExpiresHeader expiresHeader = register.getExpires();
			if (expiresHeader != null)
				requestedExpiration = expiresHeader.getExpires();
		}

		// now check if the registrar has touched our expiration timeout in its
		// response. Again, check contact headers first.
		int grantedExpiration = -1;

		ContactHeader responseContactHdr = (ContactHeader) response
				.getHeader(ContactHeader.NAME);
		if (responseContactHdr != null) {
			grantedExpiration = responseContactHdr.getExpires();
		}

		// no luck there, try the expires header.
		ExpiresHeader expiresHeader = response.getExpires();

		if (expiresHeader != null) {
			grantedExpiration = expiresHeader.getExpires();
		}
		// still no luck - let's be tolerant and reuse the expires timeout we
		// stated in our last request
		else {
			grantedExpiration = requestedExpiration;
		}

		// If this is a response to a REGISTER request ending our registration
		// then expires would be 0.
		// fix by Luca Bincoletto <Luca.Bincoletto@tilab.com>

		// we also take into account the requested expiration since if it was 0
		// we don't really care what the server replied (I have an asterisk here
		// that gives me 3600 even if I request 0).
		if (grantedExpiration <= 0 || requestedExpiration <= 0) {
			setRegistrationState(RegistrationState.UNREGISTERED,
					RegistrationStateChangeEvent.REASON_USER_REQUEST,
					"Registration terminated.");
		} else {
			setRegistrationState(RegistrationState.REGISTERED,
					RegistrationStateChangeEvent.REASON_NOT_SPECIFIED, null);
		}
	}

	/**
	 * Processes a retransmit or expiration Timeout of an underlying
	 * {@link Transaction}handled by this SipListener.
	 * 
	 * @param timeoutEvent
	 *            the timeoutEvent received indicating either the message
	 *            retransmit or transaction timed out.
	 * @return <tt>true</tt> if the specified event has been handled by this
	 *         processor and shouldn't be offered to other processors registered
	 *         for the same method; <tt>false</tt>, otherwise
	 */
	public boolean processTimeout(TimeoutEvent timeoutEvent) {
		if (getRegistrationState().equals(RegistrationState.UNREGISTERED) == false) {
			setRegistrationState(RegistrationState.CONNECTION_FAILED,
					RegistrationStateChangeEvent.REASON_NOT_SPECIFIED,
					"A timeout occurred while trying to connect to the server.");
		}
		return true;
	}

	/**
	 * Process an asynchronously reported IO Exception.
	 * 
	 * @param exceptionEvent
	 *            The Exception event that is reported to the application.
	 * @return <tt>true</tt> if the specified event has been handled by this
	 *         processor and shouldn't be offered to other processors registered
	 *         for the same method; <tt>false</tt>, otherwise
	 */
	public boolean processIOException(IOExceptionEvent exceptionEvent) {
		setRegistrationState(RegistrationState.CONNECTION_FAILED,
				RegistrationStateChangeEvent.REASON_NOT_SPECIFIED,
				"An error occurred while trying to connect to the server."
						+ "[" + exceptionEvent.getHost() + "]:"
						+ exceptionEvent.getPort() + "/"
						+ exceptionEvent.getTransport());
		return true;
	}

	/**
	 * Handles a NOT_IMPLEMENTED response sent in reply of our register request.
	 * 
	 * @param transatcion
	 *            the transaction that our initial register request belongs to.
	 * @param response
	 *            our initial register request.
	 */
	public void processNotImplemented(ClientTransaction transatcion,
			Response response) {
		setRegistrationState(
				RegistrationState.CONNECTION_FAILED,
				RegistrationStateChangeEvent.REASON_NOT_SPECIFIED,
				registrarAddress.getHostAddress()
						+ " does not appear to be a sip registrar. (Returned a "
						+ "NOT_IMPLEMENTED response to a register request)");
	}

	/**
	 * Sets our registration state to <tt>newState</tt> and dispatches an event
	 * through the protocol provider service impl.
	 * <p>
	 * 
	 * @param newState
	 *            a reference to the RegistrationState that we're currently
	 *            detaining.
	 * @param reasonCode
	 *            one of the REASON_XXX error codes specified in
	 *            {@link RegistrationStateChangeEvent}.
	 * @param reason
	 *            a reason String further explaining the reasonCode.
	 */
	void setRegistrationState(RegistrationState newState, int reasonCode,
			String reason) {
		if (currentRegistrationState.equals(newState))
			return;

		RegistrationState oldState = currentRegistrationState;
		this.currentRegistrationState = newState;

		sipMgrCallback.fireRegistrationStateChanged(oldState, newState,
				reasonCode, reason);
	}

	/**
	 * Returns the JAIN SIP provider that should be used for communication with
	 * our current registrar.
	 * 
	 * @return the JAIN SIP provider that should be used for communication with
	 *         our current registrar.
	 */
	public SipProvider getJainSipProvider() {
		return sipMgrCallback.getJainSipProvider();
	}

	/**
	 * Returns the state of this connection.
	 * 
	 * @return a RegistrationState instance indicating the state of our
	 *         registration with the corresponding registrar.
	 */
	public RegistrationState getRegistrationState() {
		return currentRegistrationState;
	}

}
